// Display.java
// Jim Sproch
// Created: May 7, 2007
// Modified: March 30, 2008
// Part of the Aforce Port
// Mac < Windows < Linux

/**
	Display is what everything shows up on.
	@author Jim Sproch
	@version 0.1a beta
*/


import java.util.*;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;  //keylistener and mouselistener

import java.awt.image.*; //for double buffering

import java.io.*; // for the "File" class
import javax.imageio.*; // for saving screen shots

// The first call to Display.start() (called by constructor) will create a new thread, which will pass control to the display
//   Once the display has aquired control, it will hold the control until Display.destroy() is called
//   The first ten seconds of control will have a splash screen, after which, the display will work normally
//   Display.stop() can be used to "freeze" an image on the screen, but control remains on display, repainting buffer constantly
//   Display.start() can be used to "unfreeze" the screen.

// I am still not quite happy with this class in general.  I feel that more of the "creation" should be pushed into the constructor
//  and the display should only pop-up at the last minute when display.start() is called.  The difficulty is that if anything is
//  pushed out of the run thread causes problems in the non-thrad-safe jframe.
public class Display implements Runnable
{

	// Because this object is Serializable because it extends JComponent
	private static final long serialVersionUID = 7526471155622776147L;

	// Action Variables
	private boolean active = false; // The display has control
	private boolean frozen = false; // The display updates the backbuffer
	public boolean splash = true; // The display shows a splash screen on startup

	// Deprecated Variables //TODO: Remove them
	public boolean on = true;
	public boolean started = true;
	public boolean returncontrol = false;

	// Instance Variables
	private JFrame frame;           // The frame (window) that pops up (main game window)
	private AForceEnv environment;  // The environment we get displayable objects from
	private Canvas canvas = null;  // The canvas we use to draw  // TODO: replace with something more applet safe
	
	// Graphics Speed Variables
	public static final long DESIRED_FPS = 30; // desired FPS (frames per second)
	public static final long DESIRED_NS_PER_FRAME = 1000000000 / DESIRED_FPS; // how long each frame should take in nanoseconds

	// Graphics Assessment Variables
	private int frameData_total = 0;
	private int frameData_count = 0;
	private long frameData_startTime = 0;

	BufferedImage nextFrame = null;
	BufferedImage lastFrame = null;

	private Size mySize = new Size(745, 580);

	Display(AForceEnv environment)
	{
		// is there a problem?  it might be that you forgot to set the display size
		//  Should be called directly after the constructor is.
		this.setEnvironment(environment);
	}

	Display()
	{
		// is there a problem?  it might be that you forgot to set the display size
		//  Should be called directly after the constructor is.
		this.setEnvironment(AForce.getField());
		this.start();
	}

	public void setEnvironment(AForceEnv environment)
	{
		this.environment = environment;
	}

	public void setDefaultCloseOperation(int operation)
	{
		if(AForce.isAppletMode()) frame.setDefaultCloseOperation(operation);
	}

	public BufferedImage getSnapShot()
	{
		return lastFrame;
	}

	// Launches a control thread to keep in the display (so we can constantly update it)
	// Sets the window as visible
	public void start()
	{
		// Freeze or unfreeze the display
		frozen = !frozen;

		// Control can only be given once
		if(!active) active = true;
		else return;

		// Launch a control thread for us to keep, pass invocation control back
		Thread thread = new Thread(this);
		AForce.getThreads().add(thread);
		thread.start();
	}

	public void saveSnapShot()
	{
		long number = System.currentTimeMillis();

		try
		{
			(new File("ScreenShots")).mkdir();
			ImageIO.write(lastFrame, "PNG", new FileOutputStream("ScreenShots"+File.separator+"AForceScreenShot"+number+".png"));
			Printer.note.println("NOTE 938: ScreenShot has been saved as: AForceScreenShot"+number+".png");
		}
		catch(Exception e)
		{
			Printer.err.println("ERROR 382: Unable to save Screen Shot (Are you in Applet/WebStart Mode?)");
		}
	}


	// It SEEMS that the thread that calls this function MUST also end up sitting in run
	//   This means that the method must remain private and only called from inside the run thread
	private void init()
	{
		// Sets some final variables so they can be used in the creation of canvas
		final int sizex = mySize.getx();
		final int sizey = mySize.gety();

		// Make a canvas
		canvas = new Canvas()
		{
			// Because this object is Serializable because it extends JComponent
			private static final long serialVersionUID = 7526471155622776147L;

			public Dimension getPreferredSize()
			{
				return new Dimension(sizex, sizey);
			}
		};

		canvas.setSize(sizex, sizey);
		
		if(!AForce.isAppletMode()) createJFrame();

		if(AForce.isAppletMode()) AForce.getGame().add(canvas);
		else frame.getContentPane().add(canvas);

		// Create a BufferStrategy in a safe way
		while(true)
		{
			boolean error = false;
			try { canvas.createBufferStrategy(2); }
			catch(IllegalStateException e) { error = true; }
			catch(Exception e) { error = true; }
			if(error == false) break;
			Thread.yield();
			try { Thread.sleep(1); } catch (Exception e) {};
		}

		nextFrame = new BufferedImage(mySize.getx(), mySize.gety(), BufferedImage.TYPE_INT_RGB);
		lastFrame = new BufferedImage(mySize.getx(), mySize.gety(), BufferedImage.TYPE_INT_RGB);
		
		// Sync up the screen (just in case)
		Toolkit.getDefaultToolkit().sync();
	}


	private void createJFrame()
	{
		// Create a JFrame
		frame = new JFrame("AForce ~ Provided by the Linux Community");
		frame.setSize(mySize.getx(), mySize.gety());
		frame.setLocation(new Point(100,100));

		// Clears the display, so we can use it!
		frame.getContentPane().removeAll();

		// Set some cool variables, like the background color
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.getContentPane().setBackground(Color.black);
		Image tempimg = (new ImageIcon(Images.class.getResource("Images/ship_23x23_2x2_red-WEST.png"))).getImage();
		frame.setIconImage(tempimg);
		frame.setResizable(false);
		//setIgnoreRepaint(true);
		javax.swing.JPopupMenu.setDefaultLightWeightPopupEnabled(false);
		canvas.setFocusable(false);
		new AForceMenu(this, environment.getAForce());
		frame.setVisible(true);
		canvas.setVisible(true);
		frame.requestFocus();
	}

	public void run()
	{
		// Initialize the display
		// This MUST be done in THIS THREAD for reasons I don't pretend to understand
		// If anyone does understand, I would be very happy if they would explain it to me
		if(canvas == null) this.init();

		// Do some checks because a graphics engine crash is difficult to recover from
		if(!canvas.isValid()) Printer.err.println("ERROR 493: The Display's Canvas is NOT valid!");
		if(!canvas.isVisible()) Printer.err.println("ERROR 493: The Display's Canvas is NOT visible!");
		while(!canvas.isVisible()) try{Thread.sleep(10);}catch(Exception e){};  // try and make it visible

		// Start Time is used to end the splash screen ontime
		long startTime = System.currentTimeMillis();

		// Display the splash screen
		while(splash && System.currentTimeMillis() - startTime < 3000)
		{
			try
			{
				frame.requestFocus();

				// Set variables used for FPS detection
				long frame_startTime = System.nanoTime();

				// Yep, we do need a few things before we get started
				BufferStrategy myStrategy = canvas.getBufferStrategy();
				Graphics graphics = myStrategy.getDrawGraphics();

				Graphics g = nextFrame.createGraphics();

				// Get an image and draw it
				Image image = (new ImageIcon(Images.class.getResource("Images/loading.png"))).getImage();
				g.drawImage(image, 0, 0, null);

				// Draw next frame to current frame (so an applet or screen-shot can get it)
				Graphics tmp = lastFrame.createGraphics();
				tmp.drawImage(nextFrame, 0, 0, null);
				tmp.dispose();

				// Draw the next frame to the back buffer
				graphics.drawImage(nextFrame, 0, 0, null);
	
				// Cleanup the drawing tool, we're done with it
				graphics.dispose();
				g.dispose();

				// Flush the buffer to the screen
				if (!myStrategy.contentsLost()) myStrategy.show();

				// if the frame took less than required time, give other threads a chance to catch up.
				waitForFrameEnd(frame_startTime);
			}
			catch(NullPointerException e) { Printer.wrn.println("Did Display.init() get called outside run thread?"); }
			catch(IllegalStateException e) { Printer.wrn.println("Did Display.init() get called outside run thread?"); }
			catch(Exception e) { Printer.wrn.println("Did Display.init() get called outside run thread?"); }
		}

		// Main painting loop
		while(true)
		{
			try
			{
				// Set variables used for FPS detection
				long frame_startTime = System.nanoTime();
	
				// Yep, we do need a few things before we get started
				BufferStrategy myStrategy = canvas.getBufferStrategy();
				ArrayList<DisplayableObject>  objects = environment.getAllDisplayableObjects();
				Graphics graphics = myStrategy.getDrawGraphics();

				Graphics g = nextFrame.createGraphics();
	
				// New buffer means we need to reset the info (like background color)
				if(!AForce.isAppletMode()) frame.getContentPane().setBackground(Color.black);
				if(!AForce.isAppletMode()) frame.setBackground(Color.black);
				g.setColor(Color.BLACK);
				g.fillRect(0, 0, (int)canvas.getSize().getWidth(), (int)canvas.getSize().getWidth());
	
				// Loop through and paint all the objects
				for(int x = 0; x < objects.size(); x++)
				{
					DisplayableObject item = objects.get(x);
					if(item.getStatus().getHealth() != 0) item.paint(g);
				}
	
				// Paint the dashboard (it is pre-drawn for us)
				AForce.getDashboard().paint(g);

				// Draw next frame to current frame (so an applet or screen-shot can get it)
				Graphics tmp = lastFrame.createGraphics();
				tmp.drawImage(nextFrame, 0, 0, null);
				tmp.dispose();

				// Draw the next frame to the back buffer
				graphics.drawImage(nextFrame, 0, 0, null);
	
				// Cleanup the drawing tool, we're done with it
				graphics.dispose();
				g.dispose();
	
				// Flush the buffer to the screen
				if (!myStrategy.contentsLost()) myStrategy.show();
	
				// Sync the display on some systems (on Linux, this fixes event queue problems)
				Toolkit.getDefaultToolkit().sync();
	
				// if the frame took less than required time, give other threads a chance to catch up.
				waitForFrameEnd(frame_startTime);
			}
			catch(NullPointerException e) { e.printStackTrace(); }
			catch(IllegalStateException e) { e.printStackTrace(); }
			catch(Exception e) { e.printStackTrace(); }
		}
	}

	public void waitForFrameEnd(long frame_startTime)
	{
		// Determine when we should be done with the frame
		long frame_endTime = frame_startTime + DESIRED_NS_PER_FRAME;

		// Yield at least once between each frame (so we don't starve all the other threads)
		Thread.yield();

		// Display a warning if we are not meeting the required FPS
		if(System.nanoTime() >= frame_endTime)
			Printer.fps.println("WARNING 249: Unable to maintain requested number of Frames per Second!");

		// Calculate some frame statistics
		frameData_total++; // total number of frames
		if(frameData_count >= DESIRED_FPS)
		{
			Printer.fps.println("FPS: 257: Printing "+DESIRED_FPS+" frames took "+(frameData_startTime - System.nanoTime())+" secs");
			frameData_startTime = 0;
			frameData_count = 0;
		}
		Printer.fps.println("FPS 854a: Frame took "+(System.nanoTime() - frame_startTime)+" nanoseconds to generate");
		Printer.fps.println("FPS 854b:   That is "+(100000000.0/(System.nanoTime() - frame_startTime))+" frames per second");

		// Spin in this loop until it's time for the next frame
		while (System.nanoTime() < frame_endTime)
		{
			// this may not be necessary, but let's be nice
			Thread.yield();

			// Sleep for a minimal amount of time
			try { Thread.sleep(1); } catch(Exception e) {}  //Do not sleep for 0ms b/c depending on platform, may not sleep
		}
	}

	public void addKeyListener(KeyListener listener)
	{
		Printer.info.println("INFO 546: Key Listener "+listener+" has been added");
		if(AForce.isAppletMode()) canvas.addKeyListener(listener);
		else frame.addKeyListener(listener);
	}

	public void addMouseListener(MouseListener listener)
	{
		Printer.info.println("INFO 547: Mouse Listener "+listener+" has been added");
		canvas.addMouseListener(listener);
	}

	public void removeMouseListener(MouseListener listener)
	{
		Printer.info.println("INFO 549: Mouse Listener "+listener+" has been removed");
		canvas.removeMouseListener(listener);
	}

	public void addFocusListener(FocusListener listener)
	{
		Printer.info.println("INFO 548: Focus Listener "+listener+" has been added");
		if(AForce.isAppletMode()) canvas.addFocusListener(listener);
		else frame.addFocusListener(listener);
	}

	public void removeFocusListener(FocusListener listener)
	{
		Printer.info.println("INFO 550: Focus Listener "+listener+" has been removed");
		if(AForce.isAppletMode()) canvas.removeFocusListener(listener);
		else frame.removeFocusListener(listener);
	}

	public KeyListener[] getKeyListeners()
	{
		if(AForce.isAppletMode()) return canvas.getKeyListeners();
		else return frame.getKeyListeners();
	}

	public MouseListener[] getMouseListeners()
	{
		return canvas.getMouseListeners();
	}

	public FocusListener[] getFocusListeners()
	{
		if(AForce.isAppletMode()) return canvas.getFocusListeners();
		else return frame.getFocusListeners();
	}

	public void removeKeyListener(KeyListener listener)
	{
		if(AForce.isAppletMode()) canvas.removeKeyListener(listener);
		else frame.removeKeyListener(listener);
	}


	public boolean hasFocus()
	{
		return (!AForce.isAppletMode() && frame != null && frame.hasFocus() || canvas != null && canvas.hasFocus());
	}

	public int getX()
	{
		return frame.getX();
	}

	public int getY()
	{
		return frame.getY();
	}

	public void setJMenuBar(JMenuBar menu)
	{
		frame.setJMenuBar(menu);
	}


	public static void main(String[] args)
	{
		Printer.noexecute();
	}

}


